import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/services.dart';

final _channel = const MethodChannel('flutter_plugin_msec');

////////////////////////////////////////////////////////////////////////////
class FlutterPluginMsec {
  static bool inited = false;

  static void init() {
    if (inited) {
      return;
    }
    inited = true;
    HttpOverrides.global = new _MsecHttpOverrides(HttpOverrides.current);
  }

  static void startEnvDetection() async {
    _channel.invokeMethod('e', null);
  }
}

////////////////////////////////////////////////////////////////////////////
class _MsecHttpOverrides extends HttpOverrides {
  HttpOverrides? _previous;

  _MsecHttpOverrides(this._previous);

  @override
  HttpClient createHttpClient(SecurityContext? context) {
    HttpClient client;
    if (_previous != null) {
      client = _previous!.createHttpClient(context);
    } else {
      client = super.createHttpClient(context);
    }
    return new _MsecHttpClient(client);
  }

  @override
  String findProxyFromEnvironment(Uri url, Map<String, String>? environment) {
    if (_previous != null) {
      return _previous!.findProxyFromEnvironment(url, environment);
    }
    return super.findProxyFromEnvironment(url, environment);
  }
}

////////////////////////////////////////////////////////////////////////////
class _MsecHttpClient implements HttpClient {
  HttpClient _client;
  @override
  bool autoUncompress = false;

  @override
  Duration? connectionTimeout;

  @override
  Duration idleTimeout = const Duration(seconds: 15);

  @override
  int? maxConnectionsPerHost;

  @override
  String? userAgent;

  _MsecHttpClient(this._client) {
    autoUncompress = _client.autoUncompress;
    connectionTimeout = _client.connectionTimeout;
    idleTimeout = _client.idleTimeout;
    maxConnectionsPerHost = _client.maxConnectionsPerHost;
    userAgent = _client.userAgent;
  }

  void _updateAttributes() {
    if (_client == null) {
      return;
    }
    _client.autoUncompress = autoUncompress;
    _client.connectionTimeout = connectionTimeout;
    _client.idleTimeout = idleTimeout;
    _client.maxConnectionsPerHost = maxConnectionsPerHost;
    _client.userAgent = userAgent;
    return;
  }

  Future<HttpClientRequest> _processRequest(Future<HttpClientRequest> future) {
    return future.then((request) async {
      Map<String, Object> map = {"a": request.uri.toString()};
      final String value = await _channel.invokeMethod('a', map);
      if (value.length > 0) {
        int pos = value.indexOf(":");
        var key = value.substring(1, pos);
        request.headers.add(key, value.substring(pos + 1));
        return new _MsecHttpClientRequest(
            request, key, int.parse(value.substring(0, 1)));
      }
      return request;
    });
  }

  @override
  void addCredentials(
      Uri url, String realm, HttpClientCredentials credentials) {
    _client.addCredentials(url, realm, credentials);
  }

  @override
  void addProxyCredentials(
      String host, int port, String realm, HttpClientCredentials credentials) {
    _client.addProxyCredentials(host, port, realm, credentials);
  }

  @override
  set authenticate(
      Future<bool> Function(Uri url, String scheme, String? realm)? f) {
    _client.authenticate = f;
  }

  @override
  set authenticateProxy(
      Future<bool> Function(
              String host, int port, String scheme, String? realm)?
          f) {
    _client.authenticateProxy = f;
  }

  @override
  set badCertificateCallback(
      bool Function(X509Certificate cert, String host, int port)? callback) {
    _client.badCertificateCallback = callback;
  }

  @override
  void close({bool force = false}) {
    _client.close(force: force);
  }

  @override
  Future<HttpClientRequest> delete(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.delete(host, port, path));
  }

  @override
  Future<HttpClientRequest> deleteUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.deleteUrl(url));
  }

  @override
  set findProxy(String Function(Uri url)? f) {
    _client.findProxy = f;
  }

  @override
  Future<HttpClientRequest> get(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.get(host, port, path));
  }

  @override
  Future<HttpClientRequest> getUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.getUrl(url));
  }

  @override
  Future<HttpClientRequest> head(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.head(host, port, path));
  }

  @override
  Future<HttpClientRequest> headUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.headUrl(url));
  }

  @override
  Future<HttpClientRequest> open(
      String method, String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.open(method, host, port, path));
  }

  @override
  Future<HttpClientRequest> openUrl(String method, Uri url) {
    _updateAttributes();
    return _processRequest(_client.openUrl(method, url));
  }

  @override
  Future<HttpClientRequest> patch(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.patch(host, port, path));
  }

  @override
  Future<HttpClientRequest> patchUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.patchUrl(url));
  }

  @override
  Future<HttpClientRequest> post(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.post(host, port, path));
  }

  @override
  Future<HttpClientRequest> postUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.postUrl(url));
  }

  @override
  Future<HttpClientRequest> put(String host, int port, String path) {
    _updateAttributes();
    return _processRequest(_client.put(host, port, path));
  }

  @override
  Future<HttpClientRequest> putUrl(Uri url) {
    _updateAttributes();
    return _processRequest(_client.putUrl(url));
  }

  @override
  set keyLog(Function(String line)? callback) {
    _client.keyLog = callback;
  }

  @override
  set connectionFactory(
      Future<ConnectionTask<Socket>> Function(
              Uri url, String? proxyHost, int? proxyPort)?
          f) {
    _client.connectionFactory = f;
  }
}

////////////////////////////////////////////////////////////////////////////
class _MsecHttpClientResponse implements HttpClientResponse {
  HttpClientResponse _response;
  Stream<List<int>> _stream;
  int _contentLength;

  _MsecHttpClientResponse(this._response, this._stream, this._contentLength);

  @override
  Future<bool> any(bool Function(List<int> element) test) {
    return _stream.any(test);
  }

  @override
  Stream<List<int>> asBroadcastStream(
      {void Function(StreamSubscription<List<int>> subscription)? onListen,
      void Function(StreamSubscription<List<int>> subscription)? onCancel}) {
    return _stream.asBroadcastStream(onListen: onListen, onCancel: onCancel);
  }

  @override
  Stream<E> asyncExpand<E>(Stream<E>? Function(List<int> event) convert) {
    return _stream.asyncExpand(convert);
  }

  @override
  Stream<E> asyncMap<E>(FutureOr<E> Function(List<int> event) convert) {
    return _stream.asyncMap(convert);
  }

  @override
  Stream<R> cast<R>() {
    return _stream.cast();
  }

  @override
  X509Certificate? get certificate => _response.certificate;

  @override
  HttpClientResponseCompressionState get compressionState =>
      _response.compressionState;

  @override
  HttpConnectionInfo? get connectionInfo => _response.connectionInfo;

  @override
  Future<bool> contains(Object? needle) {
    return _stream.contains(needle);
  }

  @override
  int get contentLength => _contentLength;

  @override
  List<Cookie> get cookies => _response.cookies;

  @override
  Future<Socket> detachSocket() {
    return _response.detachSocket();
  }

  @override
  Stream<List<int>> distinct(
      [bool Function(List<int> previous, List<int> next)? equals]) {
    return _stream.distinct(equals);
  }

  @override
  Future<E> drain<E>([E? futureValue]) {
    return _stream.drain(futureValue);
  }

  @override
  Future<List<int>> elementAt(int index) {
    return _stream.elementAt(index);
  }

  @override
  Future<bool> every(bool Function(List<int> element) test) {
    return _stream.every(test);
  }

  @override
  Stream<S> expand<S>(Iterable<S> Function(List<int> element) convert) {
    return _stream.expand(convert);
  }

  @override
  Future<List<int>> get first => _stream.first;

  @override
  Future<List<int>> firstWhere(bool Function(List<int> element) test,
      {List<int> Function()? orElse}) {
    return _stream.firstWhere(test, orElse: orElse);
  }

  @override
  Future<S> fold<S>(
      S initialValue, S Function(S previous, List<int> element) combine) {
    return _stream.fold(initialValue, combine);
  }

  @override
  Future forEach(void Function(List<int> element) action) {
    return _stream.forEach(action);
  }

  @override
  Stream<List<int>> handleError(Function onError, {bool test(error)?}) {
    return _stream.handleError(onError, test: test);
  }

  @override
  HttpHeaders get headers => _response.headers;

  @override
  bool get isBroadcast => _response.isBroadcast;

  @override
  Future<bool> get isEmpty => _response.isEmpty;

  @override
  bool get isRedirect => _response.isRedirect;

  @override
  Future<String> join([String separator = ""]) {
    return _stream.join(separator);
  }

  @override
  Future<List<int>> get last => _stream.last;

  @override
  Future<List<int>> lastWhere(bool Function(List<int> element) test,
      {List<int> Function()? orElse}) {
    return _stream.lastWhere(test, orElse: orElse);
  }

  @override
  Future<int> get length => _stream.length;

  @override
  StreamSubscription<List<int>> listen(void Function(List<int> event)? onData,
      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
    return _stream.listen(onData,
        onError: onError, onDone: onDone, cancelOnError: cancelOnError);
  }

  @override
  Stream<S> map<S>(S Function(List<int> event) convert) {
    return _stream.map(convert);
  }

  @override
  bool get persistentConnection => _response.persistentConnection;

  @override
  Future pipe(StreamConsumer<List<int>> streamConsumer) {
    return _stream.pipe(streamConsumer);
  }

  @override
  String get reasonPhrase => _response.reasonPhrase;

  @override
  Future<HttpClientResponse> redirect(
      [String? method, Uri? url, bool? followLoops]) {
    return _response.redirect(method, url, followLoops);
  }

  @override
  List<RedirectInfo> get redirects => _response.redirects;

  @override
  Future<List<int>> reduce(
      List<int> Function(List<int> previous, List<int> element) combine) {
    return _stream.reduce(combine);
  }

  @override
  Future<List<int>> get single => _stream.single;

  @override
  Future<List<int>> singleWhere(bool Function(List<int> element) test,
      {List<int> Function()? orElse}) {
    return _stream.singleWhere(test, orElse: orElse);
  }

  @override
  Stream<List<int>> skip(int count) {
    return _stream.skip(count);
  }

  @override
  Stream<List<int>> skipWhile(bool Function(List<int> element) test) {
    return _stream.skipWhile(test);
  }

  @override
  int get statusCode => _response.statusCode;

  @override
  Stream<List<int>> take(int count) {
    return _stream.take(count);
  }

  @override
  Stream<List<int>> takeWhile(bool Function(List<int> element) test) {
    return _stream.takeWhile(test);
  }

  @override
  Stream<List<int>> timeout(Duration timeLimit,
      {void Function(EventSink<List<int>> sink)? onTimeout}) {
    return _response.timeout(timeLimit, onTimeout: onTimeout);
  }

  @override
  Future<List<List<int>>> toList() {
    return _stream.toList();
  }

  @override
  Future<Set<List<int>>> toSet() {
    return _stream.toSet();
  }

  @override
  Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) {
    return _stream.transform(streamTransformer);
  }

  @override
  Stream<List<int>> where(bool Function(List<int> event) test) {
    return _stream.where(test);
  }
}

////////////////////////////////////////////////////////////////////////////
class _MsecHttpClientRequest implements HttpClientRequest {
  HttpClientRequest _request;
  String _key;
  int _enc;
  BytesBuilder? _bytesBuilder;

  @override
  bool bufferOutput = false;

  @override
  int contentLength = 0;

  @override
  late Encoding encoding;

  @override
  bool followRedirects = false;

  @override
  int maxRedirects = 0;

  @override
  bool persistentConnection = false;

  _MsecHttpClientRequest(this._request, this._key, this._enc) {
    bufferOutput = _request.bufferOutput;
    contentLength = _request.contentLength;
    encoding = _request.encoding;
    followRedirects = _request.followRedirects;
    maxRedirects = _request.maxRedirects;
    persistentConnection = _request.persistentConnection;
    if (_enc == 1) {
      _bytesBuilder = BytesBuilder();
    }
  }

  @override
  void add(List<int> data) {
    if (_enc == 1) {
      _bytesBuilder!.add(data);
    } else {
      _request.add(data);
    }
  }

  @override
  void addError(Object error, [StackTrace? stackTrace]) {
    _request.addError(error, stackTrace);
  }

  @override
  Future addStream(Stream<List<int>> stream) {
    if (_enc == 1) {
      Completer completer = Completer.sync();
      stream.listen((data) {
        add(data);
      }, onDone: () {
        completer.complete();
      });
      return completer.future;
    } else {
      return _request.addStream(stream);
    }
  }

  @override
  Future<HttpClientResponse> close() async {
    if (_enc == 1 && _bytesBuilder!.length > 0) {
      Uint8List data = _bytesBuilder!.takeBytes();
      Map<String, Object> map = {"a": uri.toString(), "b": data};
      data = await _channel.invokeMethod('d', map);
      if (data == null) {
        data = Uint8List(0);
      }
      contentLength = data.length;
      _request.contentLength = contentLength;
      _request.add(data.cast());
    }

    return _request.close().then((response) async {
      if (response.headers != null) {
        String? value = response.headers.value(_key);
        if (value == null) {
          value = '';
        }
        Map<String, Object> map = {"a": uri.toString(), "b": value};
        _channel.invokeMethod('b', map);
        if (_enc == 1) {
          Uint8List data = await _getResponseData(response);
          data = await _channel
              .invokeMethod('c', {"a": uri.toString(), "b": data});
          if (data == null) {
            data = Uint8List(0);
          }
          Stream<List<int>> stream = Stream.value(data.cast());
          return new _MsecHttpClientResponse(response, stream, data.length);
        }
      }
      return response;
    });
  }

  Future<Uint8List> _getResponseData(HttpClientResponse response) {
    Completer<Uint8List> completer = Completer<Uint8List>.sync();
    List<List<int>> chunks = <List<int>>[];
    int length = 0;
    response.listen((List<int> chunk) {
      chunks.add(chunk);
      length += chunk.length;
    }, onDone: () {
      final Uint8List bytes = Uint8List(length);
      int offset = 0;
      for (List<int> chunk in chunks) {
        bytes.setRange(offset, offset + chunk.length, chunk);
        offset += chunk.length;
      }
      completer.complete(bytes);
    }, onError: completer.completeError, cancelOnError: true);
    return completer.future;
  }

  @override
  HttpConnectionInfo? get connectionInfo => _request.connectionInfo;

  @override
  List<Cookie> get cookies => _request.cookies;

  @override
  Future<HttpClientResponse> get done => _request.done;

  @override
  Future flush() {
    return _request.flush();
  }

  @override
  HttpHeaders get headers => _request.headers;

  @override
  String get method => _request.method;

  @override
  Uri get uri => _request.uri;

  @override
  void write(Object? obj) {
    if (_enc == 1) {
      String string = '$obj';
      if (string.isEmpty) return;
      add(encoding.encode(string));
    } else {
      _request.write(obj);
    }
  }

  @override
  void writeAll(Iterable objects, [String separator = ""]) {
    if (_enc == 1) {
      Iterator iterator = objects.iterator;
      if (!iterator.moveNext()) return;
      if (separator.isEmpty) {
        do {
          write(iterator.current);
        } while (iterator.moveNext());
      } else {
        write(iterator.current);
        while (iterator.moveNext()) {
          write(separator);
          write(iterator.current);
        }
      }
    } else {
      _request.writeAll(objects, separator);
    }
  }

  @override
  void writeCharCode(int charCode) {
    if (_enc == 1) {
      write(new String.fromCharCode(charCode));
    } else {
      _request.writeCharCode(charCode);
    }
  }

  @override
  void writeln([Object? obj = ""]) {
    if (_enc == 1) {
      write(obj);
      write("\n");
    } else {
      _request.writeln(obj);
    }
  }

  @override
  void abort([Object? exception, StackTrace? stackTrace]) {
    _request.abort(exception, stackTrace);
  }
}
